﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ExaminationTT.Algorithms;
using ExaminationTT.Model;
using ExaminationTT.Rule;

namespace ConcreteAlgorithms
{
    public class AlgorithmMV : IAlgorithm
    {
        private IEnumerable<IRule> _rules;
        private IEnumerable<ICUnit> _cunits;
        private IEnumerable<IExam> _scheduledExams;
        private IEnumerable<IExam> _unscheduledExams;
        private List<DateTime> _hours;
        private IEnumerable<ISemester> _semesters;
        private List<IExamSeason> _examSeasons;
        private List<DayOfWeek> _weekDays;
        private List<IExam> _privateScheduledExams;
        
        public AlgorithmMV()
        {
            
        }

        public string Description
        {
            get { return "Algorithm MV"; }
        }

        #region IAlgorithm Members

        public void SetCUnits(IEnumerable<ICUnit> cunits)
        {
            _cunits = cunits;
        }

        public void SetRules(IEnumerable<IRule> rules)
        {
            _rules = rules;
        }

        public void SetScheduledExams(IEnumerable<IExam> exams)
        {
            _scheduledExams = exams;
        }
        /// <summary>
        /// sets the exams to schedule by seasons
        /// </summary>
        /// <param name="exams">contains the exams per season</param>
        public void SetUnScheduledExams(IEnumerable<IExam> exams)
        {
            _unscheduledExams = exams;

        }

        public void SetPossibleHours(IEnumerable<DateTime> hours)
        {
            if(hours!= null)
            _hours = hours.ToList();
        }

        public void SetPossibleWeekDays(IEnumerable<DayOfWeek> weekDays)
        {
            if(weekDays != null)
            _weekDays = weekDays.ToList();
        }

        public void SetSemesters(IEnumerable<ISemester> semesters)
        {
            _semesters = semesters;
        }

        public void SetSeasons(IEnumerable<IExamSeason> seasons)
        {
            _examSeasons = seasons.ToList() ;
        }
        public IEnumerable<IExam> Run()
        {
            //TODO validar epocas validas, dias da semana, horas
            if (_rules == null || _unscheduledExams == null || _examSeasons == null) throw new InvalidOperationException("Necessário definir regras, exames não marcados e épocas");
            
            if (_weekDays == null || _weekDays.Count == 0)
            {
                _weekDays = new List<DayOfWeek>();
                Array wd = Enum.GetValues(typeof(DayOfWeek));
                foreach (DayOfWeek item in wd)
                {
                    _weekDays.Add(item);
                }
            }
            if (_scheduledExams != null) { }
            //primeiro ordenamos por exame com maior numero de inscrições
            List<IExam> examsToSchedule = new List<IExam>();
            _privateScheduledExams = new List<IExam>();
            examsToSchedule.AddRange(_unscheduledExams);
            _privateScheduledExams.AddRange(_scheduledExams);
            
            examsToSchedule.Sort((e1, e2) => e2.CUnit.Subscriptions.Count() - e1.CUnit.Subscriptions.Count());
            //agora está ordenado por nº de alunos
            
            //definir dicionario, semestre - cadeiras
            //Dictionary<ISemester, List<IExam>> examSemesters = new Dictionary<ISemester,List<IExam>>();
            //List<IExam> multipleSemesterExams = new List<IExam>();
            //foreach (IExam exam in examsToSchedule)
            //{
                
            //    if (exam.CUnit.Semesters.Count() > 1)
            //    {
            //        multipleSemesterExams.Add(exam);
            //    }
            //    else
            //    {

            //        ISemester sem = exam.CUnit.Semesters.ToArray()[0].Semester;
            //        if (!examSemesters.ContainsKey(sem))
            //        {
            //            examSemesters.Add(sem, new List<IExam>());                        
            //        }
            //        examSemesters[sem].Add(exam);
            //    }

            //}
            //depois de termos os exames por semestre e os exames de cadeiras pertencentes a vários semestres vamos definir o espaçamento entre exames
            //isto é, se tivermos 10 cadeiras do 1º semestre e tivermos 20 dias de epoca, dá uma media de 2 dias por exame
            //Dictionary<IExamSeason, Dictionary<ISemester, double>> seasonSpacing = new Dictionary<IExamSeason, Dictionary<ISemester, double>>();
            ////TODO validar se está null
            //foreach (IExamSeason ies in _examSeasons)
            //{
            //    double totalDaysSeason = ies.EndSeason.Subtract(ies.StartSeason).TotalDays;
            //    Dictionary<ISemester, double> dicTmp = new Dictionary<ISemester, double>();
            //    seasonSpacing.Add(ies, dicTmp);
            //    foreach (ISemester sem in examSemesters.Keys)
            //    {
            //        double days = totalDaysSeason / (double)examSemesters[sem].Count;
            //        dicTmp.Add(sem, days);
            //    }
            //}
            //agora temos no seasonSpacing, por epoca, um espaçamento ideal entre semestres por cadeiras
            List<IExam> toScheduleAfter = new List<IExam>();
            //agora vamos fazer a primeira marcação para exames 
            foreach (IExamSeason ies in _examSeasons)
            {
                //agora vamos marcar por exame
                //definir como escolher hora e data
                DateTime dateExam = ies.StartSeason;
                //verificar horas de exames e hora que a epoca começa
                //dateExam = new DateTime(ies.StartSeason.Year, ies.StartSeason.Month, ies.StartSeason.Day, _hours[0].Hour, _hours[0].Minute, _hours[0].Second);
                foreach (IExam item in examsToSchedule.Where(e => e.Season == ies))
                {
                    List<IConflict> hasConflits = null;
                    if (_hours != null && _hours.Count > 0)
                    {
                        hasConflits = SetExamDate(item, _hours);
                    }
                    else
                    {
                        hasConflits = SetExamDate(item);
                    }
                    if (hasConflits != null)
                    {
                        toScheduleAfter.Add(item);
                    }
                    else
                    {
                        _privateScheduledExams.Add(item);
                    }
                }
            }
            //Dictionary<IExam, IConflit> examConflits = new Dictionary<IExam, IConflit>();
            //foreach (IExam exam in toScheduleAfter)
            //{
            //    foreach (IExam e in _privateScheduledExams)
            //    {
            //        List<IConflit> confs = null;
            //        if (_hours != null && _hours.Count > 0)
            //        {
            //            confs = SetExamDate(exam, _hours);
            //        }
            //        else
            //        {
            //            confs = SetExamDate(exam);
            //        }
            //    }
            //}

            //TODO work to schedule exames
            // agora vai-se efectuar marcação exame aquele que gerar -conflitos na totalidade. 

            return _privateScheduledExams;
        }

        private DateTime NextDay(DateTime dt)
        {

            do
            {
                dt = dt.AddDays(1);
            } while (!_weekDays.Contains(dt.DayOfWeek));
            return dt;
        }
        private List<IConflict> SetExamDate(IExam exam, IEnumerable<DateTime> hours)
        {
            DateTime dt = exam.Season.StartSeason;
            //TODO MV validate day of week
            DateTime bestSoFar = dt;
            List<IConflict> leastSevereConflit = new List<IConflict>();
            //começamos na data inicial e vamos até ao fim até encontrar melhor solução;
            exam.ExamDate = dt;

            while (exam.Season.EndSeason.Subtract(exam.ExamDate).Ticks > 0)
            {
                foreach (DateTime hour in hours)
                {
                    List<IConflict> conf = new List<IConflict>();
                    //definir hora
                    exam.ExamDate = SetHour(exam.ExamDate, hour);
                    exam.ExamEndDate = exam.ExamDate.AddHours(3);
                    foreach (IExam e in _privateScheduledExams.Where(e => e.Season == exam.Season))
                    {
                        foreach (IRule rule in _rules)
                        {
                        //primeiro vamos fazer testes a exames já marcados da mesma epoca.
                            IConflict confTest = rule.EnforceRule(exam, e);
                            if (confTest != null) conf.Add(confTest);
                        }
                    }//end of foreach exams //o exame foi "verificado" por todas as regras com todos os exames marcados
                    if (conf.Count == 0)
                    {
                        // marcação sem conflitos, retorna
                        return null;
                    }
                    else 
                    {
                        if (leastSevereConflit.Count == 0)
                        {
                            leastSevereConflit = conf;
                            bestSoFar = exam.ExamDate;
                        }
                        else
                        {

                            //compara qual a melhor lista de conflitos
                            if (CompareConflits(leastSevereConflit, conf) < 0)
                            {
                                leastSevereConflit = conf;
                                bestSoFar = exam.ExamDate;
                            }
                           
                        }
                    }

                    //caso contrario continua com a proxima hora
                }//foreach hours - horas por dia
                //passar para o proximo dia
                exam.ExamDate = NextDay(exam.ExamDate);
            }//end of while (chegou ao fim)
            //se chegou aqui é porque tem conflito, afectar a data com o conflito considerado menor
            exam.ExamDate = bestSoFar;
            exam.ExamEndDate = exam.ExamDate.AddHours(3);
            return leastSevereConflit;
        }
        /// <summary>
        /// avalia qual a lista mais favoravel, de acordo com a gravidade e seguida numero de conflitos da gravidade mais alta
        /// </summary>
        /// <param name="conflist1"></param>
        /// <param name="conflist2"></param>
        /// <returns>menor que 0 se conflist1 for menos favoravel, > 0 se conflist2 for menos favoravel</returns>
        private static int CompareConflits(List<IConflict> conflist1, List<IConflict> conflist2)
        {
            for (int i = 4; i >= 0; i--)
            {
                int conf1 = conflist1.Where(c => (int)c.Weight == i).Count();
                int conf2 = conflist2.Where(c => (int)c.Weight == i).Count();
                if (conf1 > conf2) return -1;
                if (conf1 < conf2) return 1;
            }
            

            return 0;
        }
        //metodo identico ao anterior mas sem horas. Faz marcações ao dia. 
        private List<IConflict> SetExamDate(IExam exam)
        {
            DateTime dt = exam.Season.StartSeason;
            //TODO MV validate day of week
            DateTime bestSoFar = dt;
            List<IConflict> leastSevereConflit = new List<IConflict>();
            //começamos na data inicial e vamos até ao fim até encontrar melhor solução;
            exam.ExamDate = dt;

            while (exam.Season.EndSeason.Subtract(exam.ExamDate).Ticks > 0)
            {
                List<IConflict> conf = new List<IConflict>();
                    //definir hora
                exam.ExamEndDate = exam.ExamDate.AddHours(3);
                foreach (IExam e in _privateScheduledExams.Where(e => e.Season == exam.Season))
                {
                    foreach (IRule rule in _rules)
                    {
                        //primeiro vamos fazer testes a exames já marcados da mesma epoca.
                        IConflict confTest = rule.EnforceRule(exam, e);
                        if (confTest != null) conf.Add(confTest);
                    }
                }//end of foreach exams //o exame foi "verificado" por todas as regras com todos os exames marcados
                if (conf.Count == 0)
                {
                    // marcação sem conflitos, retorna
                    return null;
                }
                else
                {
                    if (leastSevereConflit.Count == 0)
                    {
                        leastSevereConflit = conf;
                        bestSoFar = exam.ExamDate;
                    }
                    else
                    {

                        //compara qual a melhor lista de conflitos
                        if (CompareConflits(leastSevereConflit, conf) < 0)
                        {
                            leastSevereConflit = conf;
                            bestSoFar = exam.ExamDate;
                        }

                    }
                }

                //caso contrario continua com a proxima hora
                //passar para o proximo dia
                exam.ExamDate = NextDay(exam.ExamDate);
            }//end of while (chegou ao fim)
            //se chegou aqui é porque tem conflito, afectar a data com o conflito considerado menor
            exam.ExamDate = bestSoFar;
            exam.ExamEndDate = exam.ExamDate.AddHours(3);
            return leastSevereConflit;
        }
        private DateTime SetHour(DateTime date, DateTime hour)
        {
            return new DateTime(date.Year, date.Month, date.Day, hour.Hour, hour.Minute, hour.Second);
        }

        private class SemesterComparer : IEqualityComparer<ICUSemester>
        {

            #region IEqualityComparer<CUSemester> Members
            
            public bool Equals(ICUSemester x, ICUSemester y)
            {
                return x.Semester.Number == y.Semester.Number;
            }

            public int GetHashCode(ICUSemester obj)
            {
                return this.GetHashCode();
            }

            #endregion
        }
        #endregion





    }
}
